HPy · Unit 2

2.3–2.4 Style & Strings

writing code well · quotes and characters · indexing and slicing · methods · looping over strings · f-strings

2.3.1 Style

Code that works isn't the same as code that's good

A program can produce the right output and still be badly written. Style is everything about how code looks and is organized, separate from whether it runs correctly. Good style avoids bugs, saves time, saves money, and makes you more employable, so it's graded here just like correctness is. A technically correct program with poor style should not earn full marks.

Use good style the whole time you're writing, not as a coat of paint at the end. Code that started messy and gets "cleaned up" right before submission is usually still messy underneath.
Three Habits

Value it, use it, discuss it

  • Value good style. It's what makes code maintainable, debuggable, and worth other people's trust.
  • Use good style constantly. Write it in from the first line. Don't wait until the end and try to clean it up.
  • Discuss it, respectfully. Talking through style choices with classmates, after everyone has finished the exercise, is called a peer code review. It's a normal part of how professional teams write software together.
The Checklist · Part 1

Names, comments, and structure

  • Meaningful names. Give variables and functions clear names. Short names like i, j, k, and n, x, y are fine for indexes and plain numbers with no better name.
  • Comments should be brief and only added where they genuinely help. Don't narrate the obvious.
  • Helper functions break a large problem into smaller, well-named pieces.
  • Test functions give you confidence a function actually works rather than "looks good to me."
main.py
# i is a standard index name,
# so this comment is unnecessary
for i in range(len(roster)):
    checkAttendance(roster[i])
The Checklist · Part 2

Formatting, clutter, and hidden numbers

  • Formatting. Keep lines under 80 characters (most editors have a setting to show a guide line at 80 characters), indent consistently, and space things the way the course's sample code does.
  • No unused or repetitive code. Delete what you don't use. Replace near-identical repeated blocks with a helper function called several times.
  • No magic numbers. A constant whose meaning is hidden in the middle of an expression is hard to maintain. Assign it to a well-named variable instead.
  • Few or no global variables. They make code hard to read, debug, and reason about.
Magic numbers have a short exception list: -1, 0, 1, 2, and 10 are common enough to appear anywhere without a name.
FRQ · Spot the Problems

What's wrong with this function?

main.py
def c(w):
    return w*2.5+5.99

This function computes the cost of shipping a package that weighs w pounds. List at least two style-checklist violations you can find, and how you'd fix each one.

Build It · Your Turn

Rewrite it with better style

Give the function and its parameter meaningful names, and replace 2.5 and 5.99 with well-named variables. The math should stay the same: c(4) should still print 15.99.

One Good Rewrite

Same math, readable now

before
def c(w):
    return w*2.5+5.99
after
def shippingCost(weightInPounds):
    RATE_PER_POUND = 2.5
    BASE_FEE = 5.99
    return weightInPounds * RATE_PER_POUND + BASE_FEE

Nothing about what the code does changed. What changed is how quickly someone else, including future you, can tell what it does.

Checkpoint

Which rule does this break?

main.py
def area1(w, h):
    return w * h

def area2(base, height):
    return base * height
  • A Magic numbers
  • B Repetitive code that should be one helper function
  • C Overuse of global variables
  • D Meaningless names
Recap

Style

  • Style is graded alongside correctness. Working code with poor style still isn't finished code.
  • Write with good style the whole time, not as a final pass.
  • Meaningful names, light comments, helper functions, and no magic numbers or repeated blocks all make code easier for someone else, including future you, to trust and change.
2.4 Strings

Text, one character at a time

A string represents text of any kind: a name, a sentence, the contents of a file, a web page, even a Python program's own source code. Over this section we'll cover how to write string literals, the operators and functions built for strings, the character codes underneath every string, string methods, looping over strings, and formatting values into strings with f-strings.

FRQ · Warm-Up

Text you interact with every day

Name one piece of text data you interact with regularly: a username, a phone number, a search query, a song title. What's one thing a program might need to do with it: search inside it, break it into pieces, change its case, or something else?

2.4.2 String Literals

Writing text directly into code

A literal is a constant value written directly into your code, as opposed to a variable. 123, 45.6, and True are all literals. A string literal is simply a string constant, like 'Yes!'.

The literal is literally in your code.

Four Kinds of Quotes

Single-quotes are the default

  • Single-quotes ('like this') are preferred by default.
  • Double-quotes ("like this") work identically, and are handy when the text itself contains an apostrophe.
  • Triple single- or triple double-quotes ('''like this''') both work the same way. We'll just call them "triple-quotes."
main.py
print("You can't stop me!")
FRQ · Predict First

Does this line crash?

main.py
print('You can't stop me!')

Predict yes or no, and explain why.

Build It · Your Turn

Now fix it

Change the quoting so the line runs without crashing, and still prints exactly You can't stop me!

Multiline Strings

Only triple-quotes allow real newlines

Spaces, newlines, and tabs are all whitespace. A single- or double-quoted literal cannot contain an actual newline (typed with the "enter" key); doing so is a syntax error. A triple-quoted literal can, and that's the main reason triple-quotes exist.

main.py
poem = '''Code flows like water,
bugs hide in the quiet lines,
patience finds them all.'''
print(poem)
A Common Fix

Skip the leading newline

It's often more readable to start the text on its own line, right after the opening triple-quote. But that puts a newline right after the quote, which shows up as a blank first line of output. When the text is meant to line up with something printed just before it, like a boxed table sitting right under its heading, that stray blank line breaks the alignment. The fix: a backslash (\, not a forward slash) right after the opening quote tells Python to ignore that one newline.

misaligned
print('Order Summary:')
print('''
+-----------------+
| Coffee    $8.00 |
+-----------------+''')
fixed
print('Order Summary:')
print('''\
+-----------------+
| Coffee    $8.00 |
+-----------------+''')

In the misaligned version, the box floats a line below the heading it belongs with. In the fixed version, the top border lands right under Order Summary:, where it should.

Build It · Your Turn

Now align it

Fix the code so the box's top border lands right under Order Summary:, with no blank line in between.

The \n Escape Sequence

A newline inside a single quote

You can't type a real newline into a single-quoted literal, but you can write \n instead. The backslash is called the escape character, and together \n counts as one single newline character, behaving exactly like a real one.

main.py
s = "Knock knock!\nWho's there?"
print(s)
Long String Literals

Split it up, stay under 80 characters

A single very long string literal is technically legal but hard to read, and breaks the 80-character line limit. The fix: split it into shorter pieces joined with +, wrapped in parentheses so Python knows the statement continues across lines.

main.py
bio = ("Margaret Hamilton led the team that wrote "
       "the onboard flight software for the Apollo "
       "missions, and helped define what it even "
       "means to engineer software.")
print(bio)
String Constants

Every letter, ready to use

Python's string module bundles a few useful constants: every letter, every digit, all punctuation, and all whitespace, already assembled into strings for you.

main.py
import string
print(string.ascii_lowercase)  # abcd...xyz
print(string.ascii_uppercase)  # ABCD...XYZ
print(string.digits)          # 0123456789
Checkpoint 1

Which literal is correct?

Which literal correctly represents the text: She said, "Nice job!"

  • A 'She said, "Nice job!"'
  • B "She said, "Nice job!""
  • C 'She said, \'Nice job!\''
  • D She said, "Nice job!" with no quotes at all
Checkpoint 2

Two backslashes, not one

main.py
print('Row1\\nRow2')

What does this print? (There are two backslashes in a row before the n.)

  • A Row1 and Row2 on two separate lines
  • B The literal text Row1\nRow2 on one line, backslash and n included
  • C A syntax error
  • D RowRow12nn, jumbled together
Recap

String literals

  • Single-quotes are the default; double-quotes help when the text has an apostrophe.
  • Only triple-quoted literals can contain real newlines. A backslash right after the opening quote skips an unwanted leading newline.
  • \n is a single escaped newline character usable inside single- or double-quoted literals. A doubled backslash, \\n, prints as literal text instead.
  • Long literals should be split into shorter pieces joined by + inside parentheses, to stay under 80 characters per line.
2.4.3 String Operators

Building and slicing strings

Several familiar operators behave differently on strings than on numbers, and strings gain a few new operators of their own: checking whether one string contains another, and reaching in to grab individual characters or entire chunks.

+ and *

Concatenation and repetition

If s and t are strings, s + t performs concatenation: a new string with t stuck onto the end of s. If n is an integer, s * n (or n * s) performs repetition: a new string made of n copies of s.

main.py
print('ha' * 3 + '!')  # hahaha!
Try This

Repeat first, then concatenate

Line 1 finishes building s before line 2 ever runs. By the time print executes, it's just concatenating two already-known strings.

main.py
1s = 'na' * 4
2print(s + ' Batman!')
s
console
nananana Batman!
in and not in

Does one string contain another?

  • in and not in are Boolean string operators: they test whether one string occurs somewhere inside another.
  • Both are case-sensitive: uppercase never matches lowercase.
  • The empty string '' is considered to be inside every string.
main.py
print('thm' in 'rhythm')  # True
print('thy' in 'rhythm')  # False
print('RHY' in 'rhythm')  # False
print('' in 'rhythm')   # True
FRQ · Predict First

True or False, for each line?

main.py
password = 'Tr0ub4dor'
print('0' in password)
print('99' in password)
String Indexing

Every character has an address

If s is a string and i is an integer, s[i] is the character at index i, counting from the left starting at 0. Negative indexes count from the right, starting at -1. A string of length n has valid indexes 0 through n-1, and -n through -1. Anything past that crashes with string index out of range.

0P-6
1Y-5
2T-4
3H-3
4O-2
5N-1
Try This

Reading s[0], s[-1], s[2]

Compare each index against the strip on the previous slide as you watch these resolve.

main.py
1s = 'PYTHON'
2print(s[0])
3print(s[-1])
4print(s[2])
s
console
P N T
Checkpoint

What does 'PYTHON'[-3] evaluate to?

  • A 'H'
  • B 'T'
  • C 'O'
  • D It crashes
String Slicing

Grabbing a whole chunk

s[i:j] returns the part of s from index i (inclusive) to j (exclusive), just like range(). A missing start defaults to 0; a missing end defaults to the end of the string; a third value is the step.

main.py
s = 'PYTHON'
print(s[1:4])   # 'YTH'
print(s[:3])    # 'PYT'
print(s[3:])    # 'HON'
print(s[::2])   # 'PTO'
Reversing a String

A step of -1

s[::-1] reverses a string, but many programmers find it unclear at a glance. Wrapping it in a small, clearly-named helper function is better style: the trick stays in one place, and every call site reads plainly.

main.py
def reverseString(s):
    return s[::-1]

print(reverseString('python'))  # 'nohtyp'
See For Yourself · Predict First

This reverseString is broken

reverseString('python') should return 'nohtyp'. This version drops the last character with s[:-1] before reversing. Predict what it actually prints for both words, then run it.

main.py
def reverseString(s):
    return s[:-1][::-1]

print(reverseString('python'))
print(reverseString('level'))
What Went Wrong

The dropped character

s[:-1] drops s's last character before anything is reversed. But that character was supposed to become the reversed string's first character. Dropping it means it never appears anywhere in the output. The fix is simply s[::-1], reversing the whole string in one step with nothing removed first.

Slicing operations chain left to right. s[:-1][::-1] is two separate slices, applied in order, not one combined operation. Whatever the first slice removes is gone for good before the second slice even runs.
Checkpoint

What does 'ABCDEFGH'[1:7:2] evaluate to?

  • A 'BDF'
  • B 'ABC'
  • C 'BDFH'
  • D 'ACE'
Recap

String operators

  • + concatenates, * repeats. in / not in test containment, case-sensitively.
  • s[i] indexes a single character, 0-based from the left or -1-based from the right.
  • s[i:j:step] slices a range of characters, exactly like range(): start inclusive, end exclusive.
  • s[::-1] reverses a string in one step. Wrap it in a helper function for clarity.
2.4.4 String-related Builtin Functions

What a character really is

Behind every character is an integer code. This section covers len(), repr(), and the pair that connects characters to their codes: ord() and chr().

len()

Every character counts as one

len(s) returns the number of characters in s. An escape sequence like \t is typed as two symbols but is a single character, so it only counts once.

main.py
print(len('cat\tdog'))  # 7
Seeing It One Character at a Time

The tab is one cell, not two

Laid out character by character, 'cat\tdog' has exactly seven cells. The tab takes up one of them, the same as any letter.

0c
1a
2t
3\t
4d
5o
6g
FRQ · Predict First

Count every character, including newlines

main.py
'''
hi
'''
See For Yourself · Predict First

Check your prediction

Run it and see whether len(s) matches what you predicted.

main.py
s = '''
hi
'''
print(len(s))
repr()

Seeing what print() hides

repr(s) returns a computer-readable form of s: quoted, with escape sequences shown as literal text instead of acted on. It's the tool for telling whether a string has hidden whitespace that print() would otherwise render invisibly.

main.py
s = '\thi\n'
print(s)         # tab, hi, then a blank line
print(repr(s))   # '\thi\n'
Checkpoint

What does repr(s) print here?

main.py
s = ' go\n'
print(repr(s))
  • A ' go\n', quotes and escape sequence shown as text
  • B go, followed by a real blank line
  • C go
  • D A syntax error
ord() and chr()

Every character has a number

ord(s) takes a length = 1 string and returns its integer character code. chr(n) does the reverse: given the integer, it returns the one-character string. Python originally used the ASCII standard for English-keyboard characters, and later adopted Unicode, a superset covering every language.

main.py
print(ord('m'))   # 109
print(ord('Z'))   # 90
print(chr(55))    # '7'
Codes for Every Letter

CODE, character by character

Each letter's code comes from ord(). Notice how consecutive letters get consecutive codes.

C67
O79
D68
E69
Try This

Shift a letter by its code

ord() turns the letter into a number, ordinary arithmetic shifts it, and chr() turns the result back into a letter.

main.py
1letter = 'C'
2code = ord(letter)
3shifted = code + 1
4print(chr(shifted))
letter
code
shifted
console
D
Beyond the Keyboard

Characters you can't type

Unicode includes thousands of characters with no key on a standard keyboard. Lists of them are usually given in hexadecimal (base 16, using digits 09 and AF), written with a 0x prefix in Python.

main.py
star = chr(0x2605)
heart = chr(0x2764)
print(star, heart)  # ★ ❤
Try It · Explore

What does a whole range of codes give you?

Loop through a range of hex codes and print each one alongside the character it produces, including chr(0x2603). Then try a different range of your own.

Build It · Your Turn

Write shiftLetter(letter, shift)

Write shiftLetter(letter, shift), which takes a single uppercase letter and returns the letter shift positions later in the alphabet. Use ord() to get the letter's code, add shift, then use chr() to convert back. None of the test cases cross past 'Z', so you don't need to handle wraparound yet.

Recap

String-related builtin functions

  • len(s) counts characters, including escape-sequence characters like \t and \n as one each.
  • repr(s) shows a string's exact contents, escape sequences and all, which print() would otherwise hide.
  • ord(s) turns a single character into its integer code; chr(n) turns a code back into a character.
  • Unicode codes for characters outside your keyboard are usually written in hex, using 0x in Python.
2.4.5 String Methods

Asking a string to act on itself

A method is a function attached to a specific value, called with a dot instead of parentheses around the value. String methods let a string test itself, edit itself, or search itself.

Method Syntax

s.method(), not method(s)

s.upper() calls the upper() method on s. If upper() were an ordinary function, we'd write upper(s) instead. Because upper(s) only works on strings, we call it a string method.

main.py
s = 'loud'
print(s.upper())  # LOUD
Character Type Tests

What kind of characters?

  • s.islower() / s.isupper(): are all the letters lower/uppercase?
  • s.isalpha(): are all characters letters?
  • s.isdigit(): are all characters digits?
  • s.isspace(): are all characters whitespace?
main.py
print('Ticket42'.isalpha())  # False
print('PASSWORD'.isupper())  # True
print('2024'.isdigit())     # True
FRQ · Predict First

Four checks on 'Pa55word'

Predict True or False for each: .islower(), .isupper(), .isalpha(), .isdigit(), all called on 'Pa55word'.

See For Yourself · Predict First

Check your predictions

Run it and see how many of your four True/False predictions for 'Pa55word' were right.

main.py
s = 'Pa55word'
print(s.islower())
print(s.isupper())
print(s.isalpha())
print(s.isdigit())
String Edit Methods

New strings, not changed ones

  • s.lower() / s.upper(): a new string with every letter's case flipped.
  • s.replace(old, new): a new string with every occurrence of old swapped for new.
  • s.strip(): a new string with leading and trailing whitespace removed. Whitespace inside the string is untouched.
main.py
print('Loud Noises!'.replace('Noises', 'Sounds'))
s = '  quiet please  '
print(repr(s.strip()))  # 'quiet please'
Checkpoint

What does this print?

main.py
s = 'I like tea'
s.replace('tea', 'coffee')
print(s)
  • A I like tea
  • B I like coffee
  • C None
  • D It crashes
See For Yourself · Predict First

Check your prediction

Run it and see whether s still says tea.

main.py
s = 'I like tea'
s.replace('tea', 'coffee')
print(s)
Search Methods

Finding a substring

  • s.count(t): how many times t occurs in s.
  • s.startswith(t) / s.endswith(t): does s begin/end with t?
  • s.find(t): the index of t's first occurrence, or -1 if absent.
  • s.index(t): the same as find, but crashes instead of returning -1. Prefer find.
main.py
s = 'Mississippi'
print(s.count('ss'))         # 2
print(s.startswith('Miss'))  # True
print(s.find('zz'))          # -1
Checkpoint

Where do find and index disagree?

For which value of s do s.find('q') and s.index('q') behave differently?

  • A s = 'quick'
  • B s = 'queue'
  • C s = 'slow'
  • D find and index always behave the same
See For Yourself

Run every method on your own sentence

Run this as-is first, then change message to a sentence of your own and see how each result changes.

Recap

String methods

  • s.method() calls a method on the value s, distinct from an ordinary function(s) call.
  • Type-test methods (.isalpha(), .isdigit(), and friends) answer a yes/no question about a string's characters.
  • Search methods (.count(), .find(), .index()) locate a substring. Prefer .find(), since it returns -1 instead of crashing when nothing is found.
  • Edit methods (.upper(), .replace(), .strip()) always return a new string. Strings can't be changed in place.
You can NEVER change a string.
2.4.6 Looping over Strings

Visiting every character

Loops and strings pair naturally: a string is a sequence of characters, and a loop is a tool for visiting a sequence one element at a time. There are two ways to loop over a string, plus a method that splits one string into many.

for Loop without Indexes

The loop variable is the character

When you want to process each character in a string, loop directly over the string. The loop variable becomes each character in turn.

main.py
s = 'CODE'
for c in s:
    print(c)
A Real Example

Counting the vowels

count only grows when c is one of 'AEIOU'. Watch it hold steady on the consonant passes.

main.py
1s = 'CODER'
2count = 0
3for c in s:
4    if c in 'AEIOU':
5        count += 1
6print(count)
c
count
console
2
for Loop with Indexes

When you need the position too

  • range(len(s)) produces every legal index into s: 0 up to len(s) - 1.
  • The loop variable i is an index, and s[i] reaches the character at that index.
  • This form is more general: use it whenever you need the position of a character as well as the character itself.
main.py
s = 'CODE'
for i in range(len(s)):
    print(i, s[i])
Try This

Index and character, together

Watch i climb from 0 to 3, and s[i] pick out the matching character each pass.

main.py
1s = 'CODE'
2for i in range(len(s)):
3    print(i, s[i])
i
s[i]
console
i  s[i] 0  C 1  O 2  D 3  E
Checkpoint

Which loop form fits?

You need to print each character of a string together with its position, counting positions starting at 1 instead of 0. Which loop form fits?

  • A for c in s:, since you're printing characters
  • B for i in range(len(s)):, then print(i + 1, s[i])
  • C Either form works identically here
  • D Neither form can produce 1-based positions
for Loop with split()

One string, many pieces

s.split(sep) breaks s apart everywhere sep occurs, returning the pieces to loop over. The loop variable is always a string, so numeric pieces need int() or float() to convert.

main.py
data = 'Ann,88,92,79'
for item in data.split(','):
    print(item)
Try This

Sum every comma-separated value

Each item starts as a string, so int(item) converts it before adding to the running total.

main.py
1data = '10,20,5,15'
2total = 0
3for item in data.split(','):
4    total += int(item)
5print(total)
item
total
console
50
Build It · Your Turn

Total only the numbers

data now mixes words and numbers, so this crashes trying to int() a word. Add a check so the loop only adds up the numeric pieces. item.isdigit() can tell you which pieces those are.

Any Separator Works

Not just commas

split() takes whatever separator string you give it. Dates, phone numbers, and file paths are all "delimited data" once you know what to split on.

main.py
date = '2024-07-30'
for part in date.split('-'):
    print(part)  # 2024, then 07, then 30
for Loop with splitlines()

One row at a time

s.splitlines() breaks a multiline string into its individual lines, without a trailing empty line even if s ends in a newline. Combine it with split() to loop over multiline, delimited data one row at a time.

main.py
roster = '''\
Ann,88,92
Ben,75,81
Cy,95,89
'''
for line in roster.splitlines():
    print(line)
See For Yourself · Predict First

One average per student

For each line of roster, this splits on commas, pulls the name from the first piece, and averages the rest as scores. Predict all three printed lines, then run it. Try adding a fourth student to roster.

Checkpoint

What does this print?

main.py
data = '''\
x,y
z,w
'''
for row in data.splitlines():
    for item in row.split(','):
        print(item)
  • A x, y, z, w: four lines, nothing else
  • B x, y, z, w, then one extra blank line
  • C x,y then z,w: two lines, commas kept
  • D A blank line, then x, y, z, w
Recap

Looping over strings

  • for i in range(len(s)): gives you both the index and, via s[i], the character.
  • for c in s: is cleaner whenever you don't need the index, just each character.
  • s.split(sep) breaks delimited data apart; s.splitlines() breaks a multiline string into rows. Combine both to loop over multiline delimited data.
2.4.8 String Formatting with f-Strings

Dropping values straight into text

Building by concatenating strings with + and including variables works, but it's easy to misplace a space or a quote or forget to convert the variable to a string.

An f-string is a far more direct way to weave values into text.

f-String Basics

An f before the quote

Putting f right before a string's opening quote makes it an f-string. Anywhere {variable} appears inside, Python substitutes that variable's current value. The f itself is not part of the string, it's a signal to Python.

Adding an equals sign, as in {battery = }, is a shortcut for quick debugging: Python prints the variable's name and its value together.

main.py
robot = 'R2D2'
battery = 87
s = f'{robot} is at {battery = }%'
Try This

Two values combined into one string

By the time line 3 runs, both values already exist. The debug marker {battery = } is replaced with its own label and value, right inside the string.

main.py
1robot = 'R2D2'
2battery = 87
3s = f'{robot} is at {battery = }%'
robot
battery
s
Beyond Plain Variables

Braces can hold an expression

The braces in an f-string aren't limited to a bare variable name. Any expression works: arithmetic, a method call, even both together. Python evaluates it and substitutes the result.

main.py
name = 'ada'
score = 88
bonus = 5
print(f'{name.upper()}: {score + bonus}')
Quoting Rules Still Apply

Pick the quote that avoids conflict

An f-string is still a string literal underneath, so the same quoting rules apply. If the surrounding text has an apostrophe, switch the f-string's outer quotes to double, exactly as with any other literal.

main.py
marco = 'Marco'
food = 'tacos'
print(f"{marco}'s favorite food is {food}.")
Checkpoint

Which line prints it correctly?

name = 'Zoe', food = 'ramen'. Which line prints: Zoe's favorite food is ramen.

  • A print(f"{name}'s favorite food is {food}.")
  • B print(f'{name}'s favorite food is {food}.')
  • C print(f"name's favorite food is food.")
  • D print(f'name's favorite food is food.')
See For Yourself

A one-line receipt

This combines a method call, arithmetic, and an f-string in one line. The second print shows the debugging shortcut from earlier: wrapping an expression in parentheses and adding = prints its source text alongside its value. Run it, then try your own item, price, and quantity.

Recap

f-strings

  • An f right before the opening quote turns a literal into an f-string.
  • {expression} inside an f-string is evaluated and substituted, whether it's a plain variable, arithmetic, or a method call. Wrapping it in parentheses and adding an equals sign, like {(price * qty)=}, can make debugging easier: it prints the expression's own source text alongside its value.
  • f-strings still follow normal quoting rules: switch to double-quotes if the text needs an apostrophe.
Unit Recap

2.3–2.4, all together

  • Style is graded alongside correctness: meaningful names, no magic numbers, no repeated blocks, written in from the start.
  • Literals use single-quotes by default; only triple-quotes hold real newlines; \n escapes one into any string.
  • Operators +, *, in, indexing, and slicing all work on strings, indexing and slicing exactly like range().
  • ord() and chr() connect every character to an integer code underneath.
  • Methods like .upper() and .find() always return new values; strings never change in place.
  • Loops and f-strings turn character-by-character logic into readable, formatted output.
Guided Exercise · As a Class

Write encodeCaesarCipher and decodeCaesarCipher

To shift a letter by n, use the letter n positions later in the alphabet: 'a' shifted by 3 is 'd'. Once you reach the end of the alphabet, wrap back around to the beginning: 'z' shifted by 1 is 'a'. A Caesar Cipher shifts every letter in a message by the same amount, leaving non-letters unmodified and preserving each letter's case. A Caesar Cipher on 'I like zoos!' with a shift of 2 returns 'K nkmg bqqu!'.

Write encodeCaesarCipher(msg, shift), which performs a Caesar Cipher on msg, shifting each letter by shift characters. shift may be negative. Then write decodeCaesarCipher(encodedMsg, shift), which reverses a message that was encoded with that same shift. Once encodeCaesarCipher is working, decodeCaesarCipher is only a couple lines: what shift undoes a shift of shift?

Build It · Caesar Cipher

Make every assert pass

For a letter c, find its position in its own case's alphabet with ord(c) - ord('A') or ord(c) - ord('a'), add shift, wrap with % 26, then convert back with chr(). Leave any character that isn't a letter untouched.

Brython quirk: letterIndex %= 26 does not work correctly in this runner. Write it out as letterIndex = letterIndex % 26 instead.
Guided Exercise · Your Turn

Write topScorer(data)

topScorer(data) takes a multiline string of competition scores, one player per line. The first comma-separated value on a line is that player's name, guaranteed not to contain any digits. Every value after it is one non-negative score, and a player's total is the sum of every score on their line.

Return the name of the player with the highest total. If two or more players tie for the highest total, return their names as one comma-separated string, in the order they appeared in data. If data has no players at all, return the actual value None, not the string 'None'.

Build It · topScorer

Make every assert pass

data.splitlines() gives you one row per player. Split each row on commas to separate the name from the scores, and track the running leader (or leaders) as you go.

Guided Exercise · As a Class

Write isPalindrome(s)

Write the function isPalindrome(s) that returns True if s reads the same forwards and backwards, and False otherwise. Keep it case-sensitive: an uppercase letter never matches a lowercase one.

A single slicing trick from earlier in this deck solves the whole problem in one line.

Build It · isPalindrome

Make every assert pass

Guided Exercise · Your Turn

Write countVowels(s)

Write countVowels(s) that returns the number of vowels (a, e, i, o, u) in s, counted case-insensitively so both cases count. y is never counted as a vowel here.

Build It · countVowels

Make every assert pass

Guided Exercise · As a Class

Write capitalizeWords(s)

Write capitalizeWords(s) that returns s with the first letter of every word capitalized, leaving the rest of each word exactly as it was. Split s into words, rebuild each word as its capitalized first letter plus the remainder, and join the words back together with spaces.

Build It · capitalizeWords

Make every assert pass

s.split() with no argument splits on whitespace and never produces empty pieces, which keeps word[0] safe to index.

Guided Exercise · Your Turn

Write isPangram(s)

A pangram is a sentence that uses every letter of the alphabet at least once. Write isPangram(s) that returns True if s is a pangram, case-insensitively, and False otherwise.

Loop over string.ascii_lowercase, and for each letter check whether it's in s.lower().

Build It · isPangram

Make every assert pass